import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.PropertiesCredentials;
import com.amazonaws.services.cloudwatch.AmazonCloudWatchClient;
import com.amazonaws.services.cloudwatch.model.Datapoint;
import com.amazonaws.services.cloudwatch.model.Dimension;
import com.amazonaws.services.cloudwatch.model.GetMetricStatisticsRequest;
import com.amazonaws.services.cloudwatch.model.GetMetricStatisticsResult;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.Placement;
import com.amazonaws.services.ec2.model.Reservation;
import com.amazonaws.services.ec2.model.RunInstancesRequest;
import com.amazonaws.services.ec2.model.RunInstancesResult;
import com.amazonaws.services.ec2.model.TerminateInstancesRequest;

public class HeadNodeManager implements ServletContextListener
{
    AmazonEC2Client ec2;
    AmazonCloudWatchClient cloudWatch;
    String initcn = "i-29189a4f";
    String initcn_hostname = "ec2-50-19-142-97.compute-1.amazonaws.com";

    public HeadNodeManager()
    {
        AWSCredentials credentials;
        try
        {
            credentials = new PropertiesCredentials(new File("/home/ubuntu/AwsCredentials.properties"));

            ec2 = new AmazonEC2Client(credentials);
            cloudWatch = new AmazonCloudWatchClient(credentials);
        }
        catch (FileNotFoundException e)
        {
            e.printStackTrace();
        }
        catch (IllegalArgumentException e)
        {
            e.printStackTrace();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    @Override
    public void contextInitialized(ServletContextEvent arg0)
    {
        Metadata.getInstance().getComputeNodes().put(initcn, new ComputeNode(initcn_hostname));
        new Thread(new CloudWatch()).start();
        new Thread(new Elasticity()).start();
    }

    @Override
    public void contextDestroyed(ServletContextEvent arg0)
    {

    }

    private void createCN()
    {
        try
        {
            Logger.log("Attempt to create a new CN");
            RunInstancesRequest rirnew = new RunInstancesRequest(Metadata.getInstance().getCurrent_cn_image(), 1, 1);
            Placement placenew = new Placement("us-east-1c");
            rirnew.withKeyName("project");
            rirnew.withSecurityGroups("test-group");
            rirnew.withInstanceType("t1.micro");
            rirnew.withPlacement(placenew);
            Logger.log("Ready to send run instance request");
            RunInstancesResult resultnew = ec2.runInstances(rirnew);
            Instance new_ins = resultnew.getReservation().getInstances().get(0);
            Logger.log("Run instance request sent. Waiting for reply.");
            String id = new_ins.getInstanceId();
            Thread.sleep(60 * 1000);

            String hostname = "";

            DescribeInstancesResult instances = ec2.describeInstances();
            for (Reservation rev : instances.getReservations())
            {
                for (Instance inst : rev.getInstances())
                {
                    if (inst.getInstanceId().equals(id))
                    {
                        hostname = inst.getPublicDnsName();
                    }
                }
            }

            for (Application app : Metadata.getInstance().getApps())
            {
                File appfile = new File("/home/ubuntu/apps", app.filename);
                Socket soc = new Socket(hostname, FileTransferer.fileport);
                FileTransferer.send(soc, appfile, app.command);
                soc.close();
            }

            Metadata.getInstance().getComputeNodes().put(id, new ComputeNode(hostname));
            Logger.log("New CN hostname: " + hostname);
            Logger.log("New CN id: " + id);
        }
        catch (Exception e)
        {
            Logger.log(e.getLocalizedMessage());
        }
    }

    private void terminateCN(String insID)
    {
        try
        {
            Metadata.getInstance().getComputeNodes().remove(insID);
            Logger.log("Attempt to terminate " + insID);
            ArrayList<String> insList = new ArrayList<String>();
            insList.add(insID);
            TerminateInstancesRequest tir = new TerminateInstancesRequest(insList);
            ec2.terminateInstances(tir);
            Logger.log("Terminate request sent successfully"); 
        }
        catch (Exception e)
        {
            Logger.log(e.getLocalizedMessage());
        }
    }

    class Elasticity implements Runnable
    {
        @Override
        public void run()
        {
            while (true)
            {
                ConcurrentHashMap<String, ComputeNode> computeNodes = Metadata.getInstance().getComputeNodes();
                double sum = 0, min = 1;
                String victim = null;
                int count = computeNodes.size();
                for (String ins : computeNodes.keySet())
                {
                    ComputeNode cn = computeNodes.get(ins);
                    sum += cn.cpu_percent;
                    if ((victim == null || min > cn.cpu_percent) && !ins.equals(initcn) && cn.getSessions() == 0)
                    {
                        victim = ins;
                        min = cn.cpu_percent;
                    }
                }

                double avg = sum / count;
                Logger.log("CN stats: avg " + avg);

                if (avg > 0.3)
                {
                    createCN();
                }
                else if (min < 0.2 || computeNodes.size() > 1)
                {
                    terminateCN(victim);
                }

                try
                {
                    Thread.sleep(60 * 1000);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }

    }

    class CloudWatch implements Runnable
    {
        static final int watch_period = 1;

        @Override
        public void run()
        {
            while (true)
            {
                ConcurrentHashMap<String, ComputeNode> computeNodes = Metadata.getInstance().getComputeNodes();
                for (String cn : computeNodes.keySet())
                    computeNodes.get(cn).cpu_percent = watch(cn);
                try
                {
                    Thread.sleep(watch_period * 1000);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }

        private double watch(String insId)
        {
            String host = Metadata.getInstance().getComputeNodes().get(insId).hostname;
            Socket soc;
            double result = -1;
            try
            {
                soc = new Socket(host, CPUMonitor.port);
                ObjectInputStream in = new ObjectInputStream(soc.getInputStream());
                result = in.readDouble();
                in.close();
                soc.close();
            }
            catch (UnknownHostException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            catch (IOException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            return result;
        }
    }
}